﻿using System;
using System.Collections.Generic;
using System.IO;

namespace Aeon.Emulator.Sound
{
    /// <summary>
    /// Virtual device which emulates general midi playback.
    /// </summary>
    public sealed class GeneralMidi : IInputPort, IOutputPort
    {
        #region Constructors
        /// <summary>
        /// Initializes a new instance of the GeneralMidi class.
        /// </summary>
        public GeneralMidi()
        {
        }
        #endregion

        #region Public Properties
        /// <summary>
        /// Gets the current state of the General MIDI device.
        /// </summary>
        public GeneralMidiState State
        {
            get { return state; }
        }
        #endregion

        #region Private Properties
        /// <summary>
        /// Gets the current value of the MIDI status port.
        /// </summary>
        private GeneralMidiStatus Status
        {
            get
            {
                GeneralMidiStatus status = GeneralMidiStatus.OutputReady;

                if(dataBytes.Count > 0)
                    status |= GeneralMidiStatus.InputReady;

                return status;
            }
        }
        #endregion

        #region IInputPort Members
        /// <summary>
        /// Gets the input ports implemented by the device.
        /// </summary>
        IEnumerable<int> IInputPort.InputPorts
        {
            get { return new int[] { DataPort, StatusPort }; }
        }
        /// <summary>
        /// Reads a single byte from one of the device's supported ports.
        /// </summary>
        /// <param name="port">Port from which byte is read.</param>
        /// <returns>Byte read from the specified port.</returns>
        byte IInputPort.ReadByte(int port)
        {
            switch(port)
            {
            case DataPort:
                if(dataBytes.Count > 0)
                    return dataBytes.Dequeue();
                else
                    return 0;

            case StatusPort:
                return (byte)(~(byte)this.Status & 0xC0);

            default:
                throw new ArgumentException("Invalid MIDI port.", "port");
            }
        }
        /// <summary>
        /// Reads two bytes from one of the device's supported ports.
        /// </summary>
        /// <param name="port">Port from which bytes are read.</param>
        /// <returns>Bytes read from the specified port.</returns>
        ushort IInputPort.ReadWord(int port)
        {
            return ((IInputPort)this).ReadByte(port);
        }
        #endregion

        #region IOutputPort Members
        /// <summary>
        /// Gets the output ports implemented by the device.
        /// </summary>
        IEnumerable<int> IOutputPort.OutputPorts
        {
            get { return new int[] { 0x330, 0x331 }; }
        }
        /// <summary>
        /// Writes a single byte to one of the device's supported ports.
        /// </summary>
        /// <param name="port">Port where byte will be written.</param>
        /// <param name="value">Value to write to the port.</param>
        void IOutputPort.WriteByte(int port, byte value)
        {
            switch(port)
            {
            case DataPort:
                if(midiMapper == null)
                    midiMapper = new MidiMapper();
                midiMapper.SendByte(value);
                break;

            case StatusPort:
                switch(value)
                {
                case ResetCommand:
                    state = GeneralMidiState.NormalMode;
                    dataBytes.Clear();
                    dataBytes.Enqueue(CommandAcknowledge);
                    if(midiMapper != null)
                    {
                        midiMapper.Dispose();
                        midiMapper = null;
                    }
                    break;

                case EnterUartModeCommand:
                    state = GeneralMidiState.UartMode;
                    dataBytes.Enqueue(CommandAcknowledge);
                    break;
                }
                break;
            }
        }
        /// <summary>
        /// Writes two bytes to one or two of the device's supported ports.
        /// </summary>
        /// <param name="port">Port where first byte will be written.</param>
        /// <param name="value">Value to write to the ports.</param>
        void IOutputPort.WriteWord(int port, ushort value)
        {
            ((IOutputPort)this).WriteByte(port, (byte)value);
        }
        #endregion

        #region IVirtualDevice Members
        void IVirtualDevice.Pause()
        {
            if(midiMapper != null)
                midiMapper.Reset();
        }
        void IVirtualDevice.Resume()
        {
        }
        /// <summary>
        /// Invoked when the virtual device has been added to a VirtualMachine.
        /// </summary>
        /// <param name="vm">VirtualMachine which owns the device.</param>
        void IVirtualDevice.DeviceRegistered(VirtualMachine vm)
        {
        }
        /// <summary>
        /// Invoked when the device should save its current state to a <see cref="System.IO.Stream"/>.
        /// </summary>
        /// <param name="destination"><see cref="System.IO.Stream"/> to which the state is saved.</param>
        void IVirtualDevice.SaveCurrentState(Stream destination)
        {
        }
        /// <summary>
        /// Invoked when the device should restore its state from a <see cref="System.IO.Stream"/>.
        /// </summary>
        /// <param name="source"><see cref="System.IO.Stream"/> from which the state is restored.</param>
        void IVirtualDevice.RestoreState(Stream source)
        {
        }
        #endregion

        #region IDisposable Members
        /// <summary>
        /// Releases resources used by the general midi device.
        /// </summary>
        public void Dispose()
        {
            if(midiMapper != null)
            {
                midiMapper.Dispose();
                midiMapper = null;
            }
        }
        #endregion

        #region Private Fields
        private MidiMapper midiMapper;
        private readonly Queue<byte> dataBytes = new Queue<byte>();
        private GeneralMidiState state;
        #endregion

        #region Private Constants
        private const int DataPort = 0x330;
        private const int StatusPort = 0x331;
        private const byte ResetCommand = 0xFF;
        private const byte EnterUartModeCommand = 0x3F;
        private const byte CommandAcknowledge = 0xFE;
        #endregion

        #region Private Enums
        /// <summary>
        /// Specifies the current status of the General MIDI device.
        /// </summary>
        [Flags]
        private enum GeneralMidiStatus : byte
        {
            /// <summary>
            /// The status of the device is unknown.
            /// </summary>
            None = 0,
            /// <summary>
            /// The command port may be written to.
            /// </summary>
            OutputReady = (1 << 6),
            /// <summary>
            /// The data port may be read from.
            /// </summary>
            InputReady = (1 << 7)
        }
        #endregion
    }

    /// <summary>
    /// Specifies the current state of the General MIDI device.
    /// </summary>
    public enum GeneralMidiState
    {
        /// <summary>
        /// The device is in normal mode.
        /// </summary>
        NormalMode,
        /// <summary>
        /// The device is in UART mode.
        /// </summary>
        UartMode
    }
}
